/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

//=../Exception/RuntimeException
//?=Slot

/**
 * The base class for all graphical components.
 * Lacks a template and is not configured with any slots.
 * Cannot be instantiated directly and hence, is abstract.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 * @class {Container}
 */
Class.define('Container', {

  isAbstract: true,

  /**
   * @property {Object[]} slots
   */

  /**
   * @property {string|HTMLElement|jQuery}
   */
  renderTo: 'body',

  /**
   * Hiding strategy.
   *
   * @property {number}
   * @default 0
   * @example
   *   0 = display
   *   1 = visibility
   */
  hideStrategy: 0,

  /**
   * Stores a unique identifier, shared among all instances of Container.
   */
  uidStorage: {id: 0},

  /**
   * True to render into the beginning of the parent container instead of an end
   *
   * @property {bool}
   */
  prependMode: false,

  /**
   * True to automatically destroy a component once it's removed from a slot.
   *
   * @property {bool}
   */
  autoDestroy: true,

  /** @constructs */
  init: function () { this.renderTo = $(this.renderTo) },

  /**
   * Generates a unique identifier on each invocation.
   *
   * @returns {string}
   */
  uid: function () { return this.type + this.uidStorage.id++ },

  /**
   * Compiles and executes a template stored in "property".
   *
   * @param {string} property
   * @returns {string}
   */
  runTemplate: function (property) {
    if (!_.isFunction(this[property])) {
      var exc = Class.create('RuntimeException', {
        message: 'The "' + property + '" template is not valid'
      }), proto = Object.getPrototypeOf(this);
      _.isArray(this[property]) && (proto[property] = this[property].join(''));
      if (!_.isString(this[property])) throw exc;
      // compile the template only once for all instances of a component
      proto[property] = _.template(this[property]);
    }
    return this[property](this);
  },

  /**
   * Creates and returns a jQuery object containing exactly one HTMLElement.
   *
   * @returns {jQuery}
   */
  createElement: function () {
    var exc = Class.create('RuntimeException', {
      message: 'The template is not valid'
    }), el = $(this.runTemplate('tpl'));
    if (!el || (el.length !== 1) || !_.isElement(el[0])) throw exc;
    return el;
  },

  /**
   * Displays a component.
   *
   * @returns {Container}
   */
  show: function () {
    var me = this;
    if (me.el) {
      ([
        function () { me.el.show() },
        function () { me.el.css('visibility', 'visible') }
      ])[me.hideStrategy]();
    } else {
      me.el = me.createElement();
      me.fire('beforeRender');
      me.renderTo[me.prependMode ? 'prepend' : 'append'](me.el);
      // handles situations when one of the owning components is not yet
      // rendered which by proxy makes this component not rendered
      (function watch() {
        me.el.parents('body').length ? me.fire('render') :
          setTimeout(watch, 40);
      }());
    }
    me.fire('show');
    return me;
  },

  /**
   * Hides a component.
   *
   * @returns {Container}
   */
  hide: function () {
    var me = this;
    if (me.el && me.fire('beforeHide')) {
      ([
        function () { me.el.hide() },
        function () { me.el.css('visibility', 'hidden') }
      ])[me.hideStrategy]();
      me.fire('hide');
    }
    return me;
  },

  /**
   * Retrieves the first parent container or the first one specified by type.
   *
   * @param {string} [type] Class name.
   * @returns {Container}
   */
  up: function (type) {
    var owner = this.owner;
    while (owner) {
      if (Class.is('Container', owner) && !Class.is('Slot', owner) &&
          ((type === owner.type) || !type)) return owner;
      owner = owner.owner;
    }
  },

  /**
   * Animates the component with a provided class.
   *
   * @param {string} cls A css class name.
   * @param [el] An optional element to animate.
   * @returns {Container}
   */
  animate: function (cls, el) {
    cls += ' animated';
    el = el || this.el;
    if (el) {
      el = $(el);
      el.removeClass(cls).addClass(cls).one('webkitAnimationEnd mozAnimationE' +
          'nd MSAnimationEnd oanimationend animationend', function () {
        el.removeClass(cls);
      });
    }
    return this;
  },

  /**
   * Sets the width of a container.
   *
   * @param {*} width
   * @returns {Container}
   */
  setWidth: function (width) {
    if (width !== this.width) {
      this.el && this.el.width(width);
      this.width = width;
    }
    return this;
  },

  /**
   * Sets the height of a container.
   *
   * @param {*} height
   * @returns {Container}
   */
  setHeight: function (height) {
    if (height !== this.height) {
      this.el && this.el.height(height);
      this.height = height;
    }
    return this;
  },

  listeners: {
    beforeRender: function () {
      this.slots && !_.isArray(this.slots) && (this.slots = [this.slots]);
      _.each(this.slots, function (slot, i, slots) {
        var holder = slot.renderTo || this.slotHolder;
        _.isString(slot) && (slots[i] = slot = {name: slot});
        this[slot.name] = Class.create('Slot', {
          items: this[slot.name],
          renderTo: holder ? this.el.find(holder) : this.el,
          prependMode: !!slot.prependMode,
          defaults: _.extend({}, slot.defaults, this[slot.name + 'Defaults']),
          alias: slot.alias || {},
          activeItem: _.isUndefined(slot.activeItem) ? null : slot.activeItem
        }).show();
        this[slot.name].owner = this;
      }, this);
      this.css && this.el.css(this.css);
      if ('width' in this) {
        var width = this.width;
        this.width = null;
        this.setWidth(width);
      }
      if ('height' in this) {
        var height = this.height;
        this.height = null;
        this.setHeight(height);
      }
    },
    show: function () {
      _.each(this.slots, function (slot) {
        this[slot.name].fire('show');
      }, this);
    },
    hide: function () {
      _.each(this.slots, function (slot) {
        this[slot.name].fire('hide');
      }, this);
    },
    destroy: function () {
      if (this.el) {
        _.each(this.slots, function (slot) { this[slot.name].destroy() }, this);
        this.el.remove();
      }
    }
  },

  statics: {
    /**
     * Creates a setter method.
     *
     * @static
     * @param {Function} [handler] A function to call for additional changes.
     * @param {String} [param] The parameter name to write the value into.
     * @param {String} [type] A data type - "string", "bool", "number", "array".
     * @param {*} [defaultVal] A default value.
     * @returns {Function}
     */
    setter: function (handler, param, type, defaultVal) {
      _.isUndefined(defaultVal) && (defaultVal = 'string' === type ? '' :
        ('bool' === type ? false : ('number' === type ? 0 : ('array' === type ?
          [] : ''))));
      _.isString(defaultVal) && (defaultVal = '"' + defaultVal + '"');
      _.isArray(defaultVal) && (defaultVal = '[' + defaultVal + ']');
      var func = '(_.isUndefined(___p)?' + defaultVal + ':___p)';
      func = 'string' === type ? func + '||""' : ('bool' === type ? '!!' + func
        : ('number' === type ? 'Number' + func : func));
      return (new Function('___h', ['return function(___p){___p=', func,
        ';if(___p!==this.', param,
        '){___h&&this.el&&___h.call(this,___p);this.', param,
        '=___p}return this}'].join(''))(handler));
    }
  }

});